// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Linq;
using System.Text;

using Tierless.Infrastructure.Data;
using Tierless.Framework.Data;
using Tierless.Framework.Entities;
using Tierless.Framework.EntityTypes;
using Tierless.Framework.Constraints;
using Tierless.Framework.Server.Data;

namespace Tierless.Framework.Server.Constraints
{
	// TODO: Sparse key support?

	/// <summary>
	/// Provides server-side key constraint enforcement for an entity.
	/// </summary>
	public class KeyConstraint : EntityConstraint
	{
		/// <summary>
		/// Initializes a new instance of the KeyConstraint class.
		/// </summary>
		/// <param name="serverEntityType">The type of entity on which the key constraint will be placed.</param>
		/// <param name="key">The key to be enforced.</param>
		public KeyConstraint(ServerEntityType serverEntityType, EntityKey key)
			: base
			(
				String.Format("{0}KeyInsert", key.Name),
				serverEntityType.Name,
				key.Columns.Select(AItem => AItem.Name).ToArray(),
				ViolationSeverity.Error,
				String.Format("There is already an entity of type '{0}' with this ID.", serverEntityType.Name),
				ConstraintLocation.Server,
				ConstraintTransition.InsertUpdate
			)
		{
			_serverEntityType = serverEntityType;
			_key = key;
			BuildStatement();
		}
		
		private ServerEntityType _serverEntityType;
		/// <summary>
		/// Gets or sets the server entity type on which the key constraint will be enforced.
		/// </summary>
		public ServerEntityType ServerEntityType
		{
			get { return _serverEntityType; }
			set { _serverEntityType = value; }
		}
		
		private EntityKey _key;
		/// <summary>
		/// Gets or sets the key to be enforced.
		/// </summary>
		public EntityKey Key
		{
			get { return _key; }
			set { _key = value; }
		}
		
		private string _statement;

		protected override bool ShouldValidate(ConstraintContext context)
		{
			if (base.ShouldValidate(context))
			{
				if (context.Transition == ConstraintTransition.Update)
				{
					return _key.Columns.Any(AColumn => !Equals(context.Entity[AColumn.Name].OriginalValue, context.Entity[AColumn.Name].Value));
				}

				return true;
			}
			
			return false;
		}
		
		private void BuildStatement()
		{
			var statement = new StringBuilder();
			
			foreach (var column in _key.Columns)
			{
				if (statement.Length > 0)
					statement.Append(" and ");
				statement.AppendFormat("{0} = @{0}", column.Name);
			}
			
			if (_key.IsTemporal)
			{
				if (statement.Length > 0)
					statement.Append(" and ");
				statement.AppendFormat("{0} <= IsNull(@{1}, {0}) and IsNull({1}, @{0}) >= @{0}", _serverEntityType.FromDateColumnName, _serverEntityType.ToDateColumnName);
			}
			
			_statement = String.Format("select 1 from {0} with (updlock){1}{2}", EntityTypeProvider.GetEntityFullName(_serverEntityType), statement.Length == 0 ? "" : " where ", statement);
		}
		
		// NonTemporal
			// Sparse
				// on insert not exists (T where %columns% = %newvalues%)
				// on update ((%oldvalues% is null and %newvalues% is null) or %oldvalues% = %newvalues%) or not exists (T where %columns% = %newvalues%)
			// Dense
				// on insert not exists (T where (%columns% is null and %newvalues% is null) or (%columns% = %newvalues%))
				// on update ((%oldvalues% is null and %newvalues% is null) or %oldvalues% = %newvalues%) or not exists (T where (%columns% is null and %newvalues% is null) or (%columns% = %newvalues%))
		// Temporal
			// Sparse
				// on insert
					// not exists (T where %columns% = %newvalues% and FromDate <= IsNull(@ToDate, FromDate) and IsNull(ToDate, @FromDate) >= @FromDate)
				// on update
					// ((%oldvaluesandinterval% is null and %newvaluesandinterval% is null) or %oldvaluesandinterval% = %newvaluesandinterval%
						// or not exists (T where %columns% = %newvalues% and FromDate <= IsNull(@ToDate, FromDate) and IsNull(ToDate, @FromDate) >= @FromDate)
			// Dense
				// on insert
					// not exists (T where (%columns is null and %newvalues% is null) or (%columns% = %newvalues% and FromDate <= @ToDate and ToDate >= @FromDate))
				// on update
					// ((%oldvalues% is null and %newvalues% is null) or %oldvalues% = %newvalues%)
						// or not exists (T where (%columns% is null and %newvalues% is null) or (%columns% = %newvalues% and FromDate <= @ToDate and ToDate >= @FromDate))
		protected override bool IsValid(ConstraintContext context)
		{
			var paramsValue = _key.Columns.Select(AColumn => new Param(AColumn.Name, context.Entity[AColumn.Name].Value)).ToList();

			if (_key.IsTemporal)
			{
				paramsValue.Add(new Param(context.Entity.FromDateColumn.Name, context.Entity.FromDateColumn.Value));
				paramsValue.Add(new Param(context.Entity.ToDateColumn.Name, context.Entity.ToDateColumn.Value));
			}

			using (var reader = ((IServerEntityAccessor)context.EntityAccessor).Database.Open(_statement, paramsValue.ToArray()))
			{
				return (!reader.Read());
			}
		}

		protected override ConstraintViolation GetViolation(ConstraintContext context)
		{
			return 
				new ConstraintViolation
				(
					Name, 
					EntityTypeName, 
					context.ColumnNames, 
					ColumnNames, 
					context.Severity, 
					String.Format
					(
						"Entity of type '{0}' already contains this ID ({1}){2}.", 
						EntityTypeName, 
						new EntityID(context.Entity, _key), 
						_key.IsTemporal 
							? String.Format(" in the range {0} to {1}", context.Entity.FromDateColumn.AsString, context.Entity.ToDateColumn.AsString) 
							: ""
					)
				);
		}
	}
}
